Java安全[反射(3)]

  1. Java执行命令的方法ProcessBuilder
  2. 反射中使用getConstructor获取构造有参构造函数
  3. 可变长参数(varargs)在反射中的意义与使用
  4. getDeclared系列反射函数和普通反射的区别于使用

并解决第二篇的两个问题,

  • 如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?
  • 如果一个方法或构造方法是私有方法,我们是否能执行它呢?

Java安全[反射(3)]

getConstructor反射方法/ProcessBulider执行命令

第一个问题

如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?

我们需要用到一个新的反射方法getConstructor

getMethod类似,getConstructor接收参数是构造函数的列表类型。因为构造函数也支持重载,所以可能会存在多个构造函数,所以必须用参数列表类型才能唯一确认一个构造函数。

获取了构造函数后,使用newInstance来执行。

比如,我们常用的另一个执行命令的方式ProcessBulider

下面是一个简单的ProcessBuilder使用流程:

  1. 创建一个ProcessBuilder实例:

    1
    ProcessBuilder pb = new ProcessBuilder();
  2. 设置命令和参数:

    1
    pb.command("myCommand", "myArg1", "myArg2");
  3. (可选)设置其他属性,如工作目录、环境变量等。

  4. 启动进程:

    1
    Process process = pb.start();
  5. 等待进程完成并获取退出值:

    1
    int exitValue = process.waitFor();

我们使用getConstructor来获取其构造函数,然后调用start()来执行命令。

1
2
Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder) clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).start();

这里的ProcessBuilder类的构造函数有两个,而且都是有参数的

  • public ProcessBuilder(String... command)
  • public ProcessBuilder(List<String> command)

这里用到的是第二个构造函数,也可以看到构造函数的参数就是command,也就是执行的命令需要在实例化的时候传入。

image-20230909132658712

List.class和之前 前两篇提到的String.class一样,指的是调用方法的参数类型

List.class指的就算List接口类Class对象

String.class表示 String类的Class对象

在这段代码中,List.class 被用作参数传递给 getConstructor() 方法,以获取一个接受 List 类型参数的构造函数。这意味着我们正在查找一个构造函数,它接受一个 List 对象作为参数,并使用该 List 对象来初始化新创建的 ProcessBuilder 实例。于是就找到了第二个构造函数,这样,我们就可以动态地创建并启动一个新进程。


避免利用强类型转换

但是我们在payload中用到了Java中的强类型转换【((ProcessBuilder) xxx)】,有时候我们利用漏洞的时候(在表达式上下文)是没有这种语法的。所以我们仍然需要反射来完成这一步。

其中有个Arrays.asList其实也好理解,就算将参数从数组转换为列表,使其符合构造函数的参数类型,然后newInstance时将参数传进去执行。

1
((ProcessBuilder) clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).start();

其实也很好改这个payload,这里需要用到强类型转化的原因主要是因为执行.start()方法启动进程的时候,前部分不用强类型包含起来,无法找到这个方法,会在Object类中寻找。

image-20230909150718667

这里可以直接用反射中getMethod方法获取start方法,就可以避免这种问题,然后invoke执行,因为start的是一个普通方法,所以invoke的第一个参数就是ProcessBuilder类实例。

1
2
Class<?> clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe")));

image-20230909151721671

执行结果,

image-20230909152513387


可变长参数(varargs)在反射中的意义与使用

那么,如果想要用ProcessBuilder的第一个构造函数,又应该怎么实现反射呢?

1
public ProcessBuilder(String... command)

这里涉及到Java的变长参数了,和其他语言一样,Java中也支持可变长参数,就算当你在定义函数时,在设置参数时,不确定参数的个数时,可以用 ... 来表示这个函数的参数个数是可以变的。其实就是和数组差不多的含义。

而且实际上,对于变长参数,Java在编译的时候会编译成一个一维数组,也就是说,对于如下两段代码在底层上是一致的,也就是说无法重载,见下图可知。

1
2
public void hello(String[] names) {}
public void hello(String... names) {}

image-20230909154214840

也就是说,如果有个数组,想传给hello函数,直接传入数组即可

1
2
String[] names = {"hello", "world"};
hello(names);

所以,我们将字符串数组的类String[].class传给getConstructor,就可以查找获取ProcessBuilder的第二种构造函数:

1
2
class clazz = Class.forName("java.lang.ProcessBulider");
clazz.getConstructor(String[].class);

但是在通过newInstance传参时,就有不同了,因为ProcessBulider的第一个构造函数的参数是变长参数,也就是一维数组,而newInstance的参数也是变长参数,同样也是一维数组,如下图,所以想通过如果想传参成功,就是一个一维数组中元素为一维数组 ==> 也就是二维数组。

image-20230909220933269

于是构造payload如下,

1
2
Class<?> clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder) clazz.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}})).start();

这样的话,我们想要传给构造函数的参数,也就是一维数组,就被当作传给newInstance的二维数组的元素形式,传给了构造函数。

这里可能会产生一个疑惑,不是newInstance也是接收一个一维数组吗,为什么这里可以是二维数组,但是实际上这里的二维数组起的作用也只是一个一维数组,因为它的元素只能有一个一维数组。

如下,将二维数组中加入两个一维数组元素后,发生报错,期待的参数只有一个,但是却传入了两个,说明这里本质还是需要一个一维数组

image-20230910012544167

那如果只在newIntance中传入一个一维数组呢?可以看到如果直接将一个一维数组当作参数传入,newInstance就会当作传入的三个元素【"1","2","3"】都是一个数组,也就是当作传入了三个数组,而没有把整个数组当作一个对象发送给构造函数中去。

image-20230910142343462

newInstance需要的是一个对象类型的变长参数,所以只需要强类型转换将我们传入的数组整体当作一个对象类型就行。

image-20230910142657430

可以看到成功运行,不过一般情况是用不了强类型转换的,只能用反射之类的方法。

image-20230910142957441

那为什么传入二维数组的时候不用强类型转换呢?

虽然传入的是二维数组,但实际真正的对象是其元素,也就是一维数组,所以如果直接强类型把二维数组也当做对象传给newIntance反而会报错,newIntance参数类型不匹配,因为newIntrance期待的也是一个一维数组,也就是起作用的只是二维数组中那一个一维数组。

image-20230910144957733

根据p神建议将payload改成全反射方法,

1
2
Class<?> clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}}));

也是可以的,

image-20230910155334300

getDeclared

这里就是解决第二个问题

如果一个方法或构造方法是私有方法,我们是否能执行它呢?

这里就引入了一个getDeclared系列的反射,和getMethodgetConstructor区别在于

  • getMethod系列方法获取的是当前类中所有的公共方法,包括从父类继承的方法
  • getDeclared系列方法获取的是当前类中声明的方法,包括私有方法,但是是必须写在类中的,如果是从父类继承而来的就不包含了。

其中getDeclaredMethodgetDeclaredConstructor的具体用法,与getMethodgetConstructor类似,区别如上所述。

在此第二篇讲过,Runtime的构造函数是私有的,是通过静态方法Runtime.getRuntime()获取其运行实例现在了解了getDeclaredConstructor,就可以通过这个获取Runtime的私有的构造方法来实例化对象,进而执行命令。

1
2
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec",String.class).invoke(clazz.getDeclaredConstructor().newInstance(), "calc");

这里就是将

1
xx.invoke(clazz.getMethod("getRuntime").invoke(null), "calc");

替换为

1
xx.invoke(clazz.getDeclaredConstructor().newInstance(), "calc");

setAccessible

运行发生报错,

image-20230910172547619

这里报错在p神的文章中说到,这里必须要使用一个方法setAccessible,在获取到了一个私有方法后,必须用setAccessible修改器作用域,否则仍然不能调用。

所有在这里就报错提醒Runtime构造函数是私有的无法获取,只需要设置setAccessibletrue即可拥有访问域。

1
2
3
4
5
6
7
8
public class Runtime_Getdeclared {
public static void main(String[] args) throws Exception {
Class clazz = Class.forName("java.lang.Runtime");
Constructor m = clazz.getDeclaredConstructor();
m.setAccessible(true);
clazz.getMethod("exec", String.class).invoke(m.newInstance(), "calc.exe");
}
}

但是还是报错,

image-20230910174535081

问了AI才知道,

这个错误是因为Java 9引入的模块系统。在模块化Java应用程序中,一个模块只能访问到它明确打开给其他模块的包。在你的情况下,java.lang包没有被打开给你的模块,所以你不能访问它的私有成员

Java 9开始,setAccessible(true)不再总是能成功地使得私有成员可访问。如果一个包没有被打开给你的模块,那么尝试访问它的私有成员将会抛出InaccessibleObjectException

正好有个Java8,试试改一下编译器环境变量再跑一下

image-20230910205325205

可能会报错,这是因为这个项目我们已经用高版本的JDK编译过一次了,而高版本能兼容低版本的,但是低版本就无法运行高版本的,所以会报错

image-20230910213830868

https://blog.csdn.net/superit401/article/details/72731381

于是直接写个txtJava8

image-20230910220848024

发现运行成功,当然虽然是低版本,但是setAccessible还是必须存在的,

image-20230910221035690